// ==UserScript==
// @name         昱商达联-视频抓取
// @namespace    http://tampermonkey.net/
// @version      1.0.1
// @updateURL    https://gitee.com/fullband/yushangdalian-script-library/raw/master/shipin.js
// @downloadURL  https://gitee.com/fullband/yushangdalian-script-library/raw/master/shipin.js
// @description  抖音/快手：合作视频采集及上传到 ERP 系统
// @author       范腾飞
// @match        *://compass.jinritemai.com/shop/video/cooperate*
// @match        *://syt.kwaixiaodian.com/zones/collaboration/slimCollaborationDetail*
// @match        *://seller.lzsterp.com/*
// @icon         https://seller.lzsterp.com/favicon.ico
// @run-at       document-start
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_getTab
// @grant        GM_saveTab
// @grant        GM_getTabs
// ==/UserScript==

(function () {
    'use strict';

    /***************** 平台判定与常量 *****************/
    const host = location.host;
    const platform = /compass\.jinritemai\.com/.test(host) ? 'douyin'
        : /syt\.kwaixiaodian\.com/.test(host) ? 'kwai'
            : 'unknown';

    // 合作视频接口匹配正则（加强：同时精确与宽松两级）
    const douyin_cooperate_video_strict = /\/compass_api\/shop\/video\/overview\/video_list.*(?:[?&])date_type=2(?:&|$).*?(?:[?&])range_type=1/i;
    const douyin_cooperate_video_relaxed = /\/compass_api\/shop\/video\/overview\/video_list/i;

    // 统一判定函数
    function isDouyinVideoApi(url) {
        if (!url) return false;
        // || douyin_cooperate_video_relaxed.test(url)
        return douyin_cooperate_video_strict.test(url);
    }

    const kwai_cooperate_video_url_regex = /syt\.kwaixiaodian\.com\/rest\/business\/gateway\/photo\/list/i;

    // 店铺信息接口（用于拿 shop_id）
    const douyin_shop_info_regex = /ecomauth\/loginv1\/get_account_info/;
    const kwai_shop_info_regex = /rest\/business\/gateway\/shop\/evaluationV3/;

    // 上传地址（与 func.py 保持一致）
    const UPLOAD_URL = {
        //douyin: 'https://subdouyin.lzsterp.com/api/Video/Import',
        douyin: 'https://epi.lzsterp.com/api/Video/ImportVideoFromPlugin',
        kwai: 'https://epi.lzsterp.com/api/Video/ImportVideoFromPlugin'
    };

    /***************** ERP 登录（裁剪版，复用 daren.js 思路） *****************/
    const ERP_LOGIN_ORIGIN = 'https://seller.lzsterp.com';
    const ERP_LOGIN_URL = 'https://seller.lzsterp.com/#/login?redirect=/daren/myTalent';
    const ACCESS_KEY = 'lzsterp_access_token';
    const REFRESH_KEY = 'lzsterp_refresh_token';
    const ACCOUNT_KEY = 'LZSTERP_ACCOUNT';

    // === 新增：与 daren.js 保持一致的 ERP Token 方法（供上传/鉴权复用） ===
    const ERP_ACCESS_TOKEN_KEY = ACCESS_KEY;
    const ERP_REFRESH_TOKEN_KEY = REFRESH_KEY;
    function getErpAccessToken() { return gmGet(ERP_ACCESS_TOKEN_KEY, ''); }
    function getErpRefreshToken() { return gmGet(ERP_REFRESH_TOKEN_KEY, ''); }
    function setErpTokens(a, r) { if (a) gmSet(ERP_ACCESS_TOKEN_KEY, a); if (r) gmSet(ERP_REFRESH_TOKEN_KEY, r); }

    function gmGet(k, d = '') { try { return GM_getValue(k, d); } catch { return d; } }
    function gmSet(k, v) { try { GM_setValue(k, v); } catch { } }
    function gmDel(k) { try { GM_deleteValue(k); } catch { } }

    function getToken() { return gmGet(ACCESS_KEY, ''); }
    function setTokens(a, r) { if (a) gmSet(ACCESS_KEY, a); if (r) gmSet(REFRESH_KEY, r); }
    function setAccount(acc) { gmSet(ACCOUNT_KEY, acc || ''); }
    function getAccount() { return gmGet(ACCOUNT_KEY, ''); }
    function clearAuth() { [ACCESS_KEY, REFRESH_KEY, ACCOUNT_KEY, ERP_ACCESS_TOKEN_KEY, ERP_REFRESH_TOKEN_KEY].forEach(gmDel); }

    let overlayShown = false;
    let pendingReady = [];

    function hasLogin() { return !!getToken(); }
    function ensureLogin(cb) {
        if (hasLogin()) { cb && cb(); return; }
        cb && pendingReady.push(cb);
        showLoginOverlay();
    }
    function fireReady() {
        if (!hasLogin()) return;
        hideLoginOverlay();
        const list = pendingReady.slice(); pendingReady = [];
        list.forEach(f => { try { f(); } catch { } });
        updateAccountDisplay();
    }
    function showLoginOverlay() {
        if (overlayShown) return;
        overlayShown = true;
        const wrap = document.createElement('div');
        wrap.id = 'erp-login-overlay';

        Object.assign(wrap.style, {
            position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.45)', zIndex: 999999,
            display: 'flex', alignItems: 'center', justifyContent: 'center'
        });
        const box = document.createElement('div');
        Object.assign(box.style, {
            width: '880px', height: '600px', background: '#fff', borderRadius: '8px',
            boxShadow: '0 4px 18px rgba(0,0,0,0.25)', position: 'relative', overflow: 'hidden'
        });

        const tip = document.createElement('div');
        tip.textContent = '请登录 ERP 后继续';
        Object.assign(tip.style, { padding: '10px 16px', fontSize: '14px', background: '#f5f7fa', borderBottom: '1px solid #eee' });
        const close = document.createElement('div');
        close.textContent = '×';
        Object.assign(close.style, { position: 'absolute', right: '12px', top: '6px', fontSize: '24px', cursor: 'pointer', color: '#888' });
        close.onclick = () => hideLoginOverlay();
        const ifr = document.createElement('iframe');
        ifr.src = ERP_LOGIN_URL + '&_t=' + Date.now();
        Object.assign(ifr.style, { width: '100%', height: '100%', border: '0' });
        box.appendChild(close); box.appendChild(tip); box.appendChild(ifr); wrap.appendChild(box);
        document.body.appendChild(wrap);
    }
    function hideLoginOverlay() {
        const el = document.getElementById('erp-login-overlay');
        if (el) el.remove();
        overlayShown = false;
    }
    window.addEventListener('message', e => {
        if (e.origin !== ERP_LOGIN_ORIGIN) return;
        const data = e.data || {};
        if (data.type === 'lzsterp_token_set') {
            if (!hasLogin() && data.accessToken) {
                try {
                    // 同步写入两套（兼容旧 setTokens 与新 setErpTokens）
                    setTokens(data.accessToken, data.refreshToken || '');
                    setErpTokens(data.accessToken, data.refreshToken || '');
                    console.log('[shipin][login] setTokens from postMessage (fallback)');
                } catch (_) { }
            }

            if (data.account) setAccount(data.account);
            console.log('[shipin][login] message received, hasLogin=', hasLogin(), ' token.len=', getErpAccessToken().length);
            fireReady();
        }
    });

    // iframe 内登录拦截（与 daren.js 类似，裁剪）
    (function hookIframeLogin() {
        if (location.host !== 'seller.lzsterp.com') return;
        if (window.__login_hooked__) return; window.__login_hooked__ = true;
        let loginAccount = '';
        function captureBody(bodyStr) {
            try {
                const o = JSON.parse(bodyStr || '{}');
                if (o.account) loginAccount = o.account;
            } catch { }
        }
        function processResp(url, text) {
            if (!/\/api\/sysAuth\/login/.test(url)) return;
            try {
                const j = JSON.parse(text || '{}');
                if (j.code === 200 && j.result) {
                    setTokens(j.result.accessToken || '', j.result.refreshToken || '');
                    window.top.postMessage({ type: 'lzsterp_token_set', account: loginAccount }, '*');
                }
            } catch { }
        }
        const rawFetch = window.fetch;
        window.fetch = function (i, opt = {}) {
            try {
                const url = typeof i === 'string' ? i : i.url;
                const m = (opt.method || 'GET').toUpperCase();
                if (/\/api\/sysAuth\/login/.test(url) && m === 'POST' && typeof opt.body === 'string') captureBody(opt.body);
            } catch { }
            const p = rawFetch.apply(this, arguments);
            p.then(r => { if (/\/api\/sysAuth\/login/.test(r.url)) r.clone().text().then(t => processResp(r.url, t)); });
            return p;
        };
        const oOpen = XMLHttpRequest.prototype.open;
        const oSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.open = function (m, u) { this._u = u; this._m = m; return oOpen.apply(this, arguments); };
        XMLHttpRequest.prototype.send = function (b) {
            try { if (/\/api\/sysAuth\/login/.test(this._u) && (this._m || 'GET').toUpperCase() === 'POST' && typeof b === 'string') captureBody(b); } catch { }
            this.addEventListener('readystatechange', () => {
                if (this.readyState === 4) processResp(this.responseURL || this._u, this.responseText || '');
            });
            return oSend.apply(this, arguments);
        };
    })();

    /***************** 状态存储 *****************/
    const state = {
        douyinShop: { shop_id: '', shop_name: '' },
        kwaiShop: { shop_id: '', shop_name: '' },
        douyinVideos: new Map(),        // 去重用
        douyinVideosArr: [],            // 新增：保持顺序的数组(最终上传用)
        kwaiVideos: new Map(),
        lastKwaiBody: null,
        catching: false,
        isPaging: false                 // 新增：抖音翻页采集中标记
    };

    let lastDouyinPageVideos = [];   // 保留最近一次 video_list 响应对应页面的视频集合（func.py: last_request_douyin_cooperate_video_datas）

    /***************** 工具函数 *****************/
    function toast(msg, type = 'info', dur = 2500) {
        const colors = { info: '#409eff', success: '#52c41a', error: '#f56c6c', warn: '#e6a23c' };
        const el = document.createElement('div');
        el.textContent = msg;
        Object.assign(el.style, {
            position: 'fixed', top: '24px', left: '50%', transform: 'translateX(-50%)',
            background: colors[type] || colors.info, color: '#fff', padding: '8px 14px',
            fontSize: '13px', borderRadius: '4px', zIndex: 999998, boxShadow: '0 4px 12px rgba(0,0,0,.15)'
        });
        document.body.appendChild(el);
        setTimeout(() => { el.style.transition = 'all .35s'; el.style.opacity = '0'; setTimeout(() => el.remove(), 380); }, dur);
    }
    function fmtDuration(sec) {
        if (!sec || isNaN(sec)) return '';
        let h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
        const arr = [];
        if (h) arr.push(h + '时');
        if (m) arr.push(m + '分');
        if (s || !arr.length) arr.push(s + '秒');
        return arr.join('');
    }
    function tsToStr(ts) { // 秒或毫秒自适应
        if (!ts) return '';
        if (ts > 1e12) ts = Math.floor(ts / 1000);
        const d = new Date(ts * 1000);
        const p = n => String(n).padStart(2, '0');
        return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
    }
    function toCamel(o) {
        const arr = [];
        o.forEach(v => arr.push(v));
        return arr.map(it => {
            const r = {};
            Object.keys(it).forEach(k => {
                const ck = k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
                r[ck] = it[k];
            });
            return r;
        });
    }

    // ====== 修复：parseDouyinVideoList（去除未定义 _origParseDouyinVideoList 调用，并写入 lastDouyinPageVideos） ======
    // 原函数已被覆盖，这里直接替换最终版本
    // ====== 调试日志开关 ======
    const DEBUG = true;
    function debugLog(...args) { if (DEBUG) console.log('[shipin][debug]', ...args); }

    // ====== 强化 parse：空数据不清空上次缓存 ======
    function parseDouyinVideoList(json) {
        try {
            const dataRows = json?.data?.module_data?.core_data_0?.compass_general_table_value?.data || [];
            if (!Array.isArray(dataRows)) return;
            if (dataRows.length === 0) {
                debugLog('douyin list empty, keep last cache');
                return;
            }
            const pageList = [];
            dataRows.forEach(row => {
                console.log('row=', row);
                const cell = row.cell_info || {};
                const video = cell.video?.video || {};
                if(video.video_status != 1) return; // 仅保留有效
                const author = video.author || {};
                const prod = cell.product?.product || {};
                const getIdx = k => cell[k]?.index_values?.value?.value ?? 0;
                const finishRate = cell.finish_watch_rate?.index_values?.value?.value;
                const obj = {
                    distributor_id: String(author.short_id || ''),
                    distributor_name: String(author.nick_name || ''),
                    release_time: tsToStr(video.publish_time),
                    video_title: video.video_title || '',
                    video_id: String(video.video_id || ''),
                    video_link: video.video_url || '',
                    video_length: fmtDuration(video.duration || 0),
                    goods_name: prod.product_name || '',
                    product_id: prod.product_id ? String(prod.product_id) : '',
                    video_watch_count: String(getIdx('watch_cnt') || 0),
                    deal_cost: getIdx('pay_amt') || 0,
                    deal_num: getIdx('pay_cnt') || 0,
                    finish_ratio: (typeof finishRate === 'number') ? (finishRate * 100).toFixed(2) + '%' : '0.00%',
                    drainage_num: getIdx('live_pay_cnt') || 0,
                    video_type: '抖音',
                    shop_id: state.douyinShop.shop_id || ''
                };
                if (!obj.video_id) return;
                if (!state.douyinVideos.has(obj.video_id)) {
                    state.douyinVideos.set(obj.video_id, obj);
                    state.douyinVideosArr.push(obj);
                }
                pageList.push(obj);
            });
            if (pageList.length) {
                lastDouyinPageVideos = pageList;
            }
            debugLog('douyin page parsed size=', pageList.length, ' total=', state.douyinVideosArr.length, ' isPaging=', state.isPaging);
        } catch (e) {
            debugLog('parseDouyinVideoList error', e);
        }
    }

    // ==== 新增：快手视频列表解析（此前被移除导致 parseKwaiVideoList is not defined） ====
    function parseKwaiVideoList(json) {
        try {
            const list = json?.data?.data || [];
            list.forEach(item => {
                if (!item) return;
                if (item.photoStatus !== 0) return; // 仅保留有效
                const metrics = Array.isArray(item.data) ? item.data : [];
                const pick = label => {
                    const f = metrics.find(m => m.label === label);
                    return f ? f.value : (label === '完播率' ? null : 0);
                };
                const finish = pick('完播率');
                const obj = {
                    distributor_id: String(item.promoterId || ''),
                    distributor_name: String(item.promoterNick || ''),
                    release_time: (() => {
                        const ms = item.timeMillis || 0;
                        if (!ms) return '';
                        const d = new Date(ms);
                        const p = n => String(n).padStart(2, '0');
                        return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
                    })(),
                    video_title: item.caption || '',
                    video_id: String(item.photoId || ''),
                    video_link: item.mainMvUrl || '',
                    video_length: (() => {
                        const ms = item.durationMs || 0;
                        if (!ms) return '';
                        let s = Math.floor(ms / 1000);
                        const h = Math.floor(s / 3600); s %= 3600;
                        const m = Math.floor(s / 60); const sec = s % 60;
                        const arr = [];
                        if (h) arr.push(h + '时');
                        if (m) arr.push(m + '分');
                        if (sec || !arr.length) arr.push(sec + '秒');
                        return arr.join('');
                    })(),
                    goods_name: '',
                    product_id: item.itemId ? String(item.itemId) : '',
                    video_watch_count: String(pick('有效播放数') || 0),
                    deal_cost: pick('成交金额') || 0,
                    deal_num: pick('成交订单数') || 0,
                    finish_ratio: (finish != null ? finish + '%' : '0.00%'),
                    drainage_num: pick('曝光次数') || 0,
                    video_type: '快手',
                    shop_id: state.kwaiShop.shop_id || ''
                };
                if (obj.video_id && !state.kwaiVideos.has(obj.video_id)) {
                    state.kwaiVideos.set(obj.video_id, obj);
                }
            });
            // 调试
            debugLog && debugLog('[kwai] parsed batch size=', state.kwaiVideos.size);
        } catch (e) {
            debugLog && debugLog('[kwai] parse error', e);
        }
    }

    // ====== 统一 fetch 包装（避免多处重复覆盖 & 增加自愈） ======
    let originalFetch = window.fetch;
    function unifiedFetchWrapper() {
        const raw = originalFetch;
        return function (input, init = {}) {
            let url = '';
            try { url = typeof input === 'string' ? input : (input && input.url) || ''; } catch { }
            const bodySnapshot = init?.body;
            const isDouyin = isDouyinVideoApi(url);
            const isKwai = kwai_cooperate_video_url_regex.test(url);
            const isDouyinShop = douyin_shop_info_regex.test(url);
            const isKwaiShop = kwai_shop_info_regex.test(url);
            if (isKwai && bodySnapshot) {
                try {
                    const obj = typeof bodySnapshot === 'string' ? JSON.parse(bodySnapshot) : bodySnapshot;
                    const trg = obj?.param?.timeRange?.value?.[0];
                    if (trg === 'CUSTOMIZED_DAY') state.lastKwaiBody = JSON.parse(JSON.stringify(obj));
                } catch { }
            }
            const p = raw.apply(this, arguments);
            p.then(resp => {
                const ru = resp.url || url;
                if (isDouyinVideoApi(ru) || isKwai || isDouyinShop || isKwaiShop) {
                    resp.clone().text().then(txt => {
                        let j = null; try { j = JSON.parse(txt); } catch { }
                        if (isDouyinVideoApi(ru)) { debugLog('hit douyin video api:', ru); parseDouyinVideoList(j); }
                        else if (isKwai) { parseKwaiVideoList(j); }
                        else if (isDouyinShop) {
                            const d = j?.data || {}; state.douyinShop.shop_id = d.account_id || ''; state.douyinShop.shop_name = d.account_name || ''; updateAccountDisplay();
                        } else if (isKwaiShop) {
                            const d = j?.data || {}; state.kwaiShop.shop_id = d.sellerId || ''; state.kwaiShop.shop_name = d.username || ''; updateAccountDisplay();
                        }
                    }).catch(() => { });
                }
            }).catch(() => { });
            return p;
        };
    }

    function hookFetchOnce() {
        if (window.fetch === unifiedFetch) return;
        originalFetch = window.fetch;
        window.fetch = unifiedFetchWrapper();
        unifiedFetch = window.fetch; // 保存引用
        debugLog('fetch hooked');
    }
    let unifiedFetch = window.fetch;
    hookFetchOnce();

    // 自愈检测（页面后续重新赋值 fetch 时再包裹）
    setInterval(() => {
        if (window.fetch !== unifiedFetch) {
            debugLog('fetch changed by page, re-hook');
            hookFetchOnce();
        }
    }, 800);

    // ====== XHR 自愈：如果被页面脚本覆盖 open/send 重新安装（增强版） ======
    (function enhanceXHRHook() {
        const HOOK_MARK = '__shipin_wrapped';
        let lastOpen = XMLHttpRequest.prototype.open;
        let lastSend = XMLHttpRequest.prototype.send;

        function wrapXHR() {
            const proto = XMLHttpRequest.prototype;
            if (proto.open[HOOK_MARK] && proto.send[HOOK_MARK]) return; // 已包装

            const rawOpen = proto.open;
            const rawSend = proto.send;

            function wrappedOpen(method, url) {
                this.__shipin_url = url;
                this.__shipin_method = (method || 'GET').toUpperCase();
                return rawOpen.apply(this, arguments);
            }
            wrappedOpen[HOOK_MARK] = true;

            function wrappedSend(body) {
                const self = this;
                const reqBody = body;
                // 使用 loadend 更稳（无论成功失败都会触发）
                self.addEventListener('loadend', function () {
                    try {
                        const url = self.responseURL || self.__shipin_url || '';
                        if (!url) return;
                        // 店铺信息
                        if (douyin_shop_info_regex.test(url)) {
                            const j = parseJSON(self.responseText);
                            const d = j?.data || {};
                            state.douyinShop.shop_id = d.account_id || '';
                            state.douyinShop.shop_name = d.account_name || '';
                            updateAccountDisplay();
                        } else if (kwai_shop_info_regex.test(url)) {
                            const j = parseJSON(self.responseText);
                            const d = j?.data || {};
                            state.kwaiShop.shop_id = d.sellerId || '';
                            state.kwaiShop.shop_name = d.username || '';
                            updateAccountDisplay();
                        } else if (isDouyinVideoApi(url)) {
                            debugLog('XHR hit douyin video api:', url);
                            const j = parseJSON(self.responseText);
                            parseDouyinVideoList(j);
                        } else if (kwai_cooperate_video_url_regex.test(url)) {
                            const j = parseJSON(self.responseText);
                            parseKwaiVideoList(j);
                            if (reqBody && typeof reqBody === 'string') {
                                try {
                                    const obj = JSON.parse(reqBody);
                                    const trg = obj?.param?.timeRange?.value?.[0];
                                    if (trg === 'CUSTOMIZED_DAY') {
                                        state.lastKwaiBody = JSON.parse(JSON.stringify(obj));
                                    }
                                } catch { }
                            }
                        }
                    } catch (e) {
                        debugLog('XHR loadend error:', e);
                    }
                });
                return rawSend.apply(this, arguments);
            }
            wrappedSend[HOOK_MARK] = true;

            proto.open = wrappedOpen;
            proto.send = wrappedSend;
            lastOpen = proto.open;
            lastSend = proto.send;
            debugLog('XHR hooked (install or refresh)');
        }

        function parseJSON(txt) {
            if (!txt || typeof txt !== 'string') return null;
            try { return JSON.parse(txt); } catch { return null; }
        }

        function ensureXHRHook() {
            const proto = XMLHttpRequest.prototype;
            if (!proto.open[HOOK_MARK] || !proto.send[HOOK_MARK]) {
                wrapXHR();
                return;
            }
            // 若被整体替换成别的函数对象（引用变化）也重装
            if (proto.open !== lastOpen || proto.send !== lastSend) {
                wrapXHR();
            }
        }

        // 初次
        wrapXHR();
        // 自愈轮询（频率适中不影响性能）
        setInterval(ensureXHRHook, 1000);
    })();

    /***************** 抓取流程 *****************/
    async function catchVideos() {
        if (state.catching) { toast('任务进行中…', 'warn'); return; }
        state.catching = true;
        addMask('处理中...');
        try {
            if (platform === 'douyin') {
                await catchDouyinPages();
                // 使用保持顺序的数组
                const arr = state.douyinVideosArr.slice();
                await upload(arr, 'douyin');
            } else if (platform === 'kwai') {
                if (!state.lastKwaiBody) {
                    toast('请先在页面产生一次视频接口（翻页/加载）', 'error');
                } else {
                    await catchKwaiAll();
                    const arr = [...state.kwaiVideos.values()];
                    await upload(arr, 'kwai');
                }
            } else {
                toast('不支持的页面', 'error');
            }
        } catch (e) {
            console.log('[shipin] catch error', e);
            toast('抓取异常', 'error');
        } finally {
            removeMask();
            state.catching = false;
        }
    }

    async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

    // 抖音分页模拟
    async function catchDouyinPages() {
        if (!state.douyinVideosArr.length && lastDouyinPageVideos.length) {
            // 首次点击且之前已加载过当前页数据但未抓取
            lastDouyinPageVideos.forEach(v => {
                if (!state.douyinVideos.has(v.video_id)) {
                    state.douyinVideos.set(v.video_id, v);
                    state.douyinVideosArr.push(v);
                }
            });
            debugLog('prime videos from last cache size=', lastDouyinPageVideos.length);
        }
        // 备份当前展示页（抓取按钮点击前页面） -> func.py 的 last_request_douyin_cooperate_video_datas
        const cachedPage = lastDouyinPageVideos ? [...lastDouyinPageVideos] : [];
        console.log('[shipin][douyin] cached (current page) size=', cachedPage.length);

        // 点击第一页 (优先触发第一页请求, 然后清空累计, 开始采集)
        const firstPageLink = document.querySelector('#videoDetail li[title="1"] a');

        if (firstPageLink) { firstPageLink.click(); await sleep(800); }

        // 清空累计数据（与 func.py 一致）
        state.douyinVideos.clear();
        state.douyinVideosArr = [];
        state.isPaging = true;

        let guard = 0;
        while (guard < 300) { // 最多 300 次防止无限循环
            guard++;
            const nextLi = document.querySelector('#videoDetail li[title="下一页"]');
            if (!nextLi) break;
            if (nextLi.getAttribute('aria-disabled') === 'true') break;
            const btn = nextLi.querySelector('button');
            if (btn) {
                btn.click();
                await sleep(1000); // 和 func.py 中 time.sleep(1) 对齐
            } else {
                break;
            }
        }

        // 结束翻页后等待网络包全部到达（func.py 中 sleep(3)）
        await sleep(3000);
        state.isPaging = false;

        // 将备份页前置插入(保持初始展示页在最前，且不重复)——需要新数组重建
        if (cachedPage.length) {
            const existSet = new Set(state.douyinVideosArr.map(v => v.video_id));
            const prependList = [];
            for (const v of cachedPage) {
                if (!existSet.has(v.video_id)) {
                    prependList.push(v);
                    existSet.add(v.video_id);
                    state.douyinVideos.set(v.video_id, v);
                }
            }
            // 前置
            state.douyinVideosArr = prependList.concat(state.douyinVideosArr);
        }

        // 回到第一页
        if (firstPageLink) { firstPageLink.click(); }

        console.log('[shipin][douyin] final collected size=', state.douyinVideosArr.length);
    }

    // 快手分页构造请求
    async function catchKwaiAll() {
        state.kwaiVideos.clear();
        if (!state.lastKwaiBody) return;
        let tpl = JSON.parse(JSON.stringify(state.lastKwaiBody));
        //const pageSize = safeGet(() => tpl.param.pageSize.value[0]) || 10;
        const pageSize = 20;
        let pageNum = 1;
        let total = null;
        while (true) {
            tpl.param.pageNum.value[0] = pageNum;
            tpl.param.pageSize.value[0] = pageSize;
            const res = await fetchKwaiApi(tpl);
            if (!res) break;
            if (total == null) total = res?.data?.total || 0;
            parseKwaiVideoList(res);
            const got = (res?.data?.data || []).length;
            if (got === 0) break;
            const pageCount = total ? Math.ceil(total / pageSize) : (pageNum + 1);
            pageNum++;
            if (pageNum > pageCount) break;
            await sleep(400);
        }
    }

    function safeGet(fn) { try { return fn(); } catch { return undefined; } }

    async function fetchKwaiApi(body) {
        try {
            const r = await fetch(findKwaiVideoApiBase(), {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body),
                credentials: 'include'
            });
            if (!r.ok) return null;
            return await r.json();
        } catch { return null; }
    }

    function findKwaiVideoApiBase() {
        // 固定接口（根据监听）
        return 'https://syt.kwaixiaodian.com/rest/business/gateway/photo/list';
    }

    /***************** 上传 *****************/
    async function upload(list, plat) {
        if (!list.length) { toast('未采集到数据', 'warn'); return; }
        const camel = toCamel(list);
        // 新增: 确保 shopId 为字符串
        camel.forEach(o => {
            if (o.hasOwnProperty('shopId')) {
                o.shopId = (o.shopId === undefined || o.shopId === null) ? '' : String(o.shopId);
            }
        });
        const url = UPLOAD_URL[plat];
        const bodyStr = JSON.stringify(camel);

        function gmPost(u, data) {
            return new Promise((resolve, reject) => {
                if (typeof GM_xmlhttpRequest !== 'function') return reject('NO_GM');
                try {
                    GM_xmlhttpRequest({
                        method: 'POST',
                        url: u + '?' + 'videotype=' + (plat == 'douyin' ? '抖音' : '快手'),
                        headers: {
                            'Content-Type': 'application/json',
                            'Accept': 'application/json',
                            'Authorization': 'Bearer ' + getErpAccessToken()
                        },
                        data: data,
                        timeout: 60000,
                        onload: r => resolve({ status: r.status, text: r.responseText || '' }),
                        onerror: e => reject(e),
                        ontimeout: e => reject(e)
                    });
                } catch (e) { reject(e); }
            });
        }
        async function fetchPost(u, data) {
            const r = await fetch(u, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Accept': 'application/json',
                    'Authorization': 'Bearer ' + getErpAccessToken()
                },
                body: data
            });
            return { status: r.status, text: await r.text() };
        }


        try {
            let resp;
            try {
                resp = await gmPost(url, bodyStr);
                
                let j = {};
                try { j = JSON.parse(resp.text || '{}'); } catch (_) { }

                if (resp.status === 401 || j.code === 401 || resp.status === 403 || j.code === 403) {
                    clearAuth();
                    updateAccountDisplay();
                    toast('登录已失效，请重新登录', 'error');
                    showLoginOverlay();
                    return;
                }

                if (resp.status === 200 && j.code === 200) {
                    // 统计唯一 videoId
                    const count = new Set(camel.map(v => v.videoId)).size;
                    const insertNum = j?.result?.insertNum || 0;
                    const updateNum = j?.result?.updateNum || 0;
                    toast(`上传成功 ${count} 条有效视频（新增 ${insertNum} 条，更新 ${updateNum} 条）`, 'success', 3200);
                }else{
                    const emsg = j?.message || (j?.result || {}).msg || '未知错误';
                    //toast('上传失败：' + emsg, 'error');
                    toast('上传失败：' + "请联系管理员", 'error');
                }

            } catch (_) {
                resp = await fetchPost(url, bodyStr);
                if (resp.status !== 200) {
                    toast('上传失败(' + resp.status + ')', 'error');
                    console.log('[shipin] upload fail', resp);
                    return;
                }
                if(resp.status === 401 || j.code === 401){
                    clearAuth();
                    updateAccountDisplay();
                    toast('登录已失效，请重新登录', 'error');
                    showLoginOverlay();
                    return;
                }
            }
            
        } catch (e) {
            toast('上传异常', 'error');
        }
    }

    /***************** UI：底部操作栏 *****************/
    function createBar() {
        if (!pageReadyForMonitor()) return;
        if (document.getElementById('shipin-bottom-bar')) return;
        if (platform === 'unknown') return;
        const bar = document.createElement('div');
        bar.id = 'shipin-bottom-bar';
        Object.assign(bar.style, {
            position: 'fixed', bottom: '0', left: '0', width: '100%', background: '#fff',
            borderTop: '1px solid #e8e8e8', boxShadow: '0 -2px 8px rgba(0,0,0,.08)',
            padding: '10px 18px', display: 'flex', alignItems: 'center', zIndex: 9999,
            boxSizing: 'border-box', fontSize: '14px', justifyContent: 'space-between'
        });

        const left = document.createElement('div');
        const userSpan = document.createElement('span');
        userSpan.id = 'erp-user-display';
        userSpan.textContent = '用户：' + (getAccount() || '未登录');
        Object.assign(userSpan.style, { marginRight: '16px' });
        const logout = document.createElement('button');
        logout.textContent = '退出登录';
        styleBtn(logout, false);
        logout.onclick = () => {
            clearAuth();
            updateAccountDisplay();
            toast('已退出', 'info');
        };
        left.appendChild(userSpan);
        left.appendChild(logout);

        const mid = document.createElement('div');
        const catchBtn = document.createElement('button');
        catchBtn.id = 'catch-video-btn';
        catchBtn.textContent = '抓取视频';
        styleBtn(catchBtn, true);
        catchBtn.onclick = () => {
            ensureLogin(() => {
                catchVideos();
            });
        };
        mid.appendChild(catchBtn);

        const right = document.createElement('div');
        const hideBtn = document.createElement('button');
        hideBtn.textContent = '隐藏';
        styleBtn(hideBtn, false);
        hideBtn.onclick = () => {
            bar.remove();
            showShowBtn();
        };
        right.appendChild(hideBtn);

        bar.appendChild(left); bar.appendChild(mid); bar.appendChild(right);
        document.body.appendChild(bar);
        updateAccountDisplay();
    }

    function showShowBtn() {
        if (document.getElementById('shipin-show-bar-btn')) return;
        const b = document.createElement('button');
        b.id = 'shipin-show-bar-btn';
        b.textContent = '显示操作栏';
        Object.assign(b.style, {
            position: 'fixed', bottom: '10px', left: '14px', zIndex: 9999,
            background: '#f54545', color: '#fff', border: '1px solid #f54545',
            padding: '6px 14px', borderRadius: '4px', cursor: 'pointer'
        });
        b.onclick = () => { b.remove(); createBar(); };
        document.body.appendChild(b);
    }

    function styleBtn(btn, primary) {
        Object.assign(btn.style, {
            border: '1px solid', padding: '0 20px', height: '34px', lineHeight: '34px',
            borderRadius: '4px', cursor: 'pointer', fontSize: '14px',
            background: primary ? '#f54545' : '#fff',
            color: primary ? '#fff' : '#333',
            borderColor: primary ? '#f54545' : '#dcdfe6',
            marginRight: '12px'
        });
        btn.onmouseenter = () => btn.style.filter = 'brightness(0.95)';
        btn.onmouseleave = () => btn.style.filter = 'none';
    }

    function updateAccountDisplay() {
        const el = document.getElementById('erp-user-display');
        if (el) el.textContent = '用户：' + (getAccount() || '未登录');
    }

    /***************** 处理蒙版 *****************/
    function addMask(text) {
        removeMask();
        const m = document.createElement('div');
        m.id = 'shipin-mask';
        Object.assign(m.style, {
            position: 'fixed', inset: '0', background: 'rgba(0,0,0,.4)', zIndex: 999998,
            display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff',
            fontSize: '24px', fontWeight: '600'
        });
        m.textContent = text || '处理中...';
        document.body.appendChild(m);
    }
    function removeMask() {
        const m = document.getElementById('shipin-mask');
        if (m) m.remove();
    }

    /***************** 入口：等待 DOM 可用后建栏 *****************/
    function isValidPage() {
        if (platform === 'douyin') return /\/shop\/video\/cooperate/.test(location.href);
        if (platform === 'kwai') return /slimCollaborationDetail/.test(location.href);
        return false;
    }

    const readyTimer = setInterval(() => {
        if (document.readyState === 'interactive' || document.readyState === 'complete') {
            if (isValidPage()) createBar();
            clearInterval(readyTimer);
        }
    }, 120);

    // 路由变化重新挂栏
    setInterval(() => {
        if (isValidPage() && !document.getElementById('shipin-bottom-bar') && !document.getElementById('shipin-show-bar-btn')) {
            createBar();
        }
    }, 300);

    // ========= 新增：水印逻辑（对应 func.py AddWatermark / RemoveWatermark） =========
    function addWatermark() {
        if (document.getElementById('monitoring_watermark')) return;
        const wm = document.createElement('div');
        wm.id = 'monitoring_watermark';
        Object.assign(wm.style, {
            position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%) rotate(-30deg)',
            fontSize: '5vw', color: 'rgba(255,0,0,0.15)', zIndex: 99999, pointerEvents: 'none', whiteSpace: 'nowrap'
        });
        wm.textContent = '监听中...';
        document.body.appendChild(wm);
    }
    function removeWatermark() {
        const wm = document.getElementById('monitoring_watermark');
        if (wm) wm.remove();
    }

    // ========= 新增：页面状态检测（参照 func.py 中条件） =========
    function pageReadyForMonitor() {
        if (platform === 'douyin') {
            if (document.title !== '合作视频') return false;
            const sel = document.querySelector('#videoDetail span.ecom-select-selection-item');
            return !!(sel && sel.textContent.trim() === '自然日新发布视频');
        }
        if (platform === 'kwai') {
            if (document.title !== '快手小店生意通') return false;
            const root = document.getElementById('dilu_micro_root');
            if (!root) return false;
            // 是否出现“合作内容明细”字样
            const hasDetail = /合作内容明细/.test(root.innerText || '');
            // 当前选中的“自然日”快捷时间标签（id=quickActive）
            const natureEl = root.querySelector('#quickActive');
            const isNatureDay = natureEl && /自然日/.test(natureEl.textContent.trim());
            return !!(hasDetail && isNatureDay);
        }
        return false;
    }

    // ========= 修改：createBar 仅在状态满足时调用 =========
    const _origCreateBar = createBar;
    createBar = function () {
        if (!pageReadyForMonitor()) return;
        _origCreateBar();
    };

    // ========= 修改：抓取抖音流程，合并 lastDouyinPageVideos =========
    const _origCatchDouyinPages = catchDouyinPages;
    catchDouyinPages = async function () {
        if (!state.douyinVideosArr.length && lastDouyinPageVideos.length) {
            // 首次点击且之前已加载过当前页数据但未抓取
            lastDouyinPageVideos.forEach(v => {
                if (!state.douyinVideos.has(v.video_id)) {
                    state.douyinVideos.set(v.video_id, v);
                    state.douyinVideosArr.push(v);
                }
            });
            debugLog('prime videos from last cache size=', lastDouyinPageVideos.length);
        }
        // 备份当前展示页（抓取按钮点击前页面） -> func.py 的 last_request_douyin_cooperate_video_datas
        const cachedPage = lastDouyinPageVideos ? [...lastDouyinPageVideos] : [];
        console.log('[shipin][douyin] cached (current page) size=', cachedPage.length);

        // 点击第一页 (优先触发第一页请求, 然后清空累计, 开始采集)
        const firstPageLink = document.querySelector('#videoDetail li[title="1"] a');

        if (firstPageLink) { firstPageLink.click(); await sleep(800); }

        // 清空累计数据（与 func.py 一致）
        state.douyinVideos.clear();
        state.douyinVideosArr = [];
        state.isPaging = true;

        let guard = 0;
        while (guard < 300) { // 最多 300 次防止无限循环
            guard++;
            const nextLi = document.querySelector('#videoDetail li[title="下一页"]');
            if (!nextLi) break;
            if (nextLi.getAttribute('aria-disabled') === 'true') break;
            const btn = nextLi.querySelector('button');
            if (btn) {
                btn.click();
                await sleep(1000); // 和 func.py 中 time.sleep(1) 对齐
            } else {
                break;
            }
        }

        // 结束翻页后等待网络包全部到达（func.py 中 sleep(3)）
        await sleep(3000);
        state.isPaging = false;

        // 将备份页前置插入(保持初始展示页在最前，且不重复)——需要新数组重建
        if (cachedPage.length) {
            const existSet = new Set(state.douyinVideosArr.map(v => v.video_id));
            const prependList = [];
            for (const v of cachedPage) {
                if (!existSet.has(v.video_id)) {
                    prependList.push(v);
                    existSet.add(v.video_id);
                    state.douyinVideos.set(v.video_id, v);
                }
            }
            // 前置
            state.douyinVideosArr = prependList.concat(state.douyinVideosArr);
        }

        // 回到第一页
        if (firstPageLink) { firstPageLink.click(); }

        console.log('[shipin][douyin] final collected size=', state.douyinVideosArr.length);
    };

    // ========= MutationObserver 版本的 SPA 视图监控（替换原 route/location 方案，避免需刷新） =========
    (function installMutationObserverInit(){
        if (window.__shipin_mo_installed__) return;
        window.__shipin_mo_installed__ = true;

        debugLog('[shipin][MO] install');

        // 1. 关闭旧的基于路由/interval 的初始化策略（若已存在）
        try {
            // 覆盖 shipin-location-change 监听逻辑为空
            window.removeEventListener && window.removeEventListener('shipin-location-change', ()=>{});
            window.addEventListener('shipin-location-change', function(){ /* noop by MO */ });
        } catch(_) {}

        // 覆盖 attemptPageInit / safeReinit 成空操作（由 MutationObserver 统一调度）
        try {
            if (typeof attemptPageInit === 'function') {
                attemptPageInit = function(){ debugLog('[shipin][MO] attemptPageInit blocked'); };
            }
            if (typeof safeReinit === 'function') {
                const _old = safeReinit;
                safeReinit = function(reason){
                    debugLog('[shipin][MO] safeReinit redirected reason=', reason);
                    // 依旧允许 shop_id 触发时做一次视图重建
                    scheduleCheck();
                };
            }
        } catch(_) {}

        // 尝试清理可能存在的旧 interval（最佳努力）
        try { routeInitTimer && clearInterval(routeInitTimer); } catch(_) {}
        try { readyTimer && clearInterval(readyTimer); } catch(_) {}

        // 2. 去抖
        let __moTimer = null;
        function scheduleCheck(){
            if(__moTimer) return;
            __moTimer = setTimeout(()=>{
                __moTimer = null;
                checkAndRun();
            },120);
        }

        // 3. 初始化函数（每个新视图只执行一次）
        function initView(){
            debugLog('[shipin][MO] initView on href=', location.href);
            try {
                createBar();      // 内部已判断 pageReadyForMonitor
                addWatermark();   // 内部已防重复
            } catch(e){
                debugLog('[shipin][MO] initView error', e);
            }
        }

        // 4. 判定 + 哨兵
        function checkAndRun(){
            if(!pageReadyForMonitor()){
                // 如果之前初始化过且现在条件不满足，可选择移除水印/操作栏（保持原逻辑）
                return;
            }
            let sentinel = document.getElementById('shipin-view-sentinel');
            const curHref = location.href;
            if(!sentinel){
                sentinel = document.createElement('div');
                sentinel.id = 'shipin-view-sentinel';
                sentinel.style.display = 'none';
                sentinel.dataset.href = curHref;
                document.documentElement.appendChild(sentinel);
                initView();
                return;
            }
            if(sentinel.dataset.href !== curHref){
                sentinel.dataset.href = curHref;
                initView();
            }
        }

        // 5. 立即尝试一次
        checkAndRun();

        // 6. 安装 MutationObserver
        function startObserve(){
            if(!document.body){
                setTimeout(startObserve, 50);
                return;
            }
            const observer = new MutationObserver(()=> scheduleCheck());
            observer.observe(document.body, {childList:true, subtree:true});
            debugLog('[shipin][MO] observer started');
        }
        startObserve();

        // 7. 监听 URL 的 hash / popstate（辅助触发，避免某些框架仅修改 history.state 不变动 DOM）
        window.addEventListener('popstate', scheduleCheck);
        window.addEventListener('hashchange', scheduleCheck);

        // 8. 定时兜底（极端情况下 DOM 不变化但状态已变）
        setInterval(()=> scheduleCheck(), 1500);
    })();

    // 移除重复定义: 已有 getErpAccessToken 上方保留, 删除下方重复版本
    // (原脚本下方再次定义的 getErpAccessToken 已被删除)

    // ====== 自愈 & 重初始化增强 (处理账号/店铺切换同 URL 不刷新场景) ======
    let __shipin_unloaded = false;
    window.addEventListener('beforeunload', () => { __shipin_unloaded = true; });

    function safeReinit(reason){
        if(platform !== 'douyin') return;
        debugLog('[shipin] safeReinit reason=', reason);
        resetDouyinContextIfNeeded();
        forceRehook();
        attemptPageInit(true); // 强制再次尝试
    }

    // 重写 attemptPageInit 允许重复触发(force=true时无视 routePending)
    const __origAttemptPageInit = attemptPageInit;
    attemptPageInit = function(force=false){
        if(force){
            routePending = false;
            clearInterval(routeInitTimer);
        }
        __origAttemptPageInit();
    };

    // storage 事件 (token / 账号变化 可能来自其它标签或脚本)
    window.addEventListener('storage', (e)=>{
        if(platform==='douyin' && (e.key||'').includes('access_token')){
            safeReinit('storage_token_change');
        }
    });

    // 页面重新可见时自检
    document.addEventListener('visibilitychange', ()=>{
        if(document.visibilityState==='visible'){
            if(platform==='douyin' && !document.getElementById('shipin-bottom-bar') && pageReadyForMonitor()){
                safeReinit('visibility_return');
            }
        }
    });

    // 监听店铺信息 shop_id 变化自动 reinit
    const __origSetDouyinShopId = state.douyinShop ? state.douyinShop.shop_id : '';
    let __lastDouyinShopId = __origSetDouyinShopId;
    // 在 fetch/XHR 解析店铺信息后追加检测（统一包一层: 覆盖 updateAccountDisplay 之后更安全）
    const __origUpdateAccountDisplay = updateAccountDisplay;
    updateAccountDisplay = function(){
        __origUpdateAccountDisplay && __origUpdateAccountDisplay();
        if(platform==='douyin'){
            const cur = state.douyinShop.shop_id || '';
            if(cur && __lastDouyinShopId && cur !== __lastDouyinShopId){
                safeReinit('shop_id_changed');
            }
            if(cur) __lastDouyinShopId = cur;
        }
    };

    // MutationObserver 监控核心容器出现 (抖音 #videoDetail)
    if(platform==='douyin'){
        const mo = new MutationObserver(()=>{
            if(__shipin_unloaded) return;
            if(!document.getElementById('shipin-bottom-bar') && pageReadyForMonitor()){
                safeReinit('mutation_container_ready');
            }
        });
        mo.observe(document.documentElement, {subtree:true, childList:true});
    }

    // 扩展 route 监听: 也检测 search/hash 变化
    (function enhanceHrefWatch(){
        let lastFull = location.href;
        setInterval(()=>{
            if(__shipin_unloaded) return;
            if(location.href !== lastFull){
                lastFull = location.href;
                window.dispatchEvent(new Event('shipin-location-change'));
            }
        }, 400);
    })();

    // 停止核心轮询防止页面卸载后仍跑(减少潜在 No tab with id)
    const __mainPollStopper = setInterval(()=>{
        if(__shipin_unloaded){
            try{
                clearInterval(__mainPollStopper);
                // 可选择清除其它长轮询: routeInitTimer / readyTimer 等
            }catch{}
        }
    },1000);

    /************ 路由 & 店铺切换自愈（解决切换店铺后需刷新才能生效的问题） ************/
    (function installRouteWatcher(){
        if (window.__shipin_route_watch__) return;
        window.__shipin_route_watch__ = true;

        const _push = history.pushState;
        const _rep  = history.replaceState;
        function fireRouteChange(){
            window.dispatchEvent(new Event('shipin-location-change'));
        }
        history.pushState = function(){
            const r = _push.apply(this, arguments);
            fireRouteChange();
            return r;
        };
        history.replaceState = function(){
            const r = _rep.apply(this, arguments);
            fireRouteChange();
            return r;
        };
        window.addEventListener('popstate', fireRouteChange);

        // 兜底：href 轮询对付站内脚本未用 history API 的场景
        let lastHref = location.href;
        setInterval(()=>{
            if (location.href !== lastHref){
                lastHref = location.href;
                fireRouteChange();
            }
        }, 500);
    })();

    // 重置抖音上下文（切换店铺 / 路由后重新抓取）
    function resetDouyinContextIfNeeded(){
        if (platform !== 'douyin') return;
        state.douyinVideos.clear();
        state.douyinVideosArr = [];
        lastDouyinPageVideos = [];
        state.isPaging = false;
        debugLog('[shipin] douyin context reset (route/shop change)');
    }

    // 强制重新 Hook（若页面重写 fetch / XHR）
    function forceRehook(){
        try{
            hookFetchOnce(); // 已有自愈，这里手动触发一次
        }catch{}
        // XHR 自愈周期已存在，如需立刻强制可简单触发一个虚拟请求或重新执行 enhanceXHRHook，不重复此处
    }

    let routeInitTimer = null;
    let routeInitGuard = 0;
    let routePending = false;

    function attemptPageInit(){
        if (routePending) return;
        routePending = true;
        routeInitGuard = 0;
        clearInterval(routeInitTimer);
        routeInitTimer = setInterval(()=>{
            routeInitGuard++;
            // 等待页面真正进入监控条件（标题 + “自然日新发布视频” 选项）
            if (pageReadyForMonitor()){
                debugLog('[shipin] route init success, pageReadyForMonitor true');
                createBar();
                addWatermark();
                routePending = false;
                clearInterval(routeInitTimer);
            }else{
                // 超时放弃（15 秒）
                if (routeInitGuard > 150){
                    debugLog('[shipin] route init timeout, give up this round');
                    routePending = false;
                    clearInterval(routeInitTimer);
                }
            }
        }, 100);
    }

    window.addEventListener('shipin-location-change', ()=>{
        // 只关注进入抖音合作视频页
        if (platform === 'douyin' && /\/shop\/video\/cooperate/.test(location.pathname)){
            debugLog('[shipin] detected route/shop change -> reinit try');
            resetDouyinContextIfNeeded();
            forceRehook();
            attemptPageInit();
        }
    });

    // 初次进入若稍后 SPA 才渲染，也跑一次兜底（避免页面极晚才插入筛选 DOM）
    if (platform === 'douyin'){
        setTimeout(()=>{
            if (!document.getElementById('shipin-bottom-bar')){
                attemptPageInit();
            }
        }, 2000);
    }

    /***************** TabId 监测（替换：使用 GM_getTab/GM_getTabs，移除 chrome 依赖） *****************/
    (function installTabIdWatcher(){
        if (window.__shipin_tabid_installed__) return;
        window.__shipin_tabid_installed__ = true;

        const TAB_KEY = '__shipin_tab_uid';
        const LISTENERS = [];
        let lastEmitted = null;

        function genId(){
            return 'T-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2,8);
        }

        function emit(id){
            if(!id || id === lastEmitted) return;
            lastEmitted = id;
            window.__SHIPIN_TAB_ID = id;
            LISTENERS.forEach(fn => { try{ fn(id); }catch{} });
            // 业务自愈：tab 识别变化后可按需触发
            try {
                if (platform === 'douyin' && typeof safeReinit === 'function' && pageReadyForMonitor()){
                    safeReinit('tab_id_changed_gm');
                }
            } catch {}
            debugLog('[shipin][tabId] emit ->', id);
        }

        async function ensureTabId(){
            try{
                const tab = await GM_getTab();
                if(!tab[TAB_KEY]){
                    tab[TAB_KEY] = genId();
                    await GM_saveTab(tab);
                }
                emit(tab[TAB_KEY]);
            }catch{
                // 退化：本地生成一次
                emit(window.__SHIPIN_TAB_ID || genId());
            }
        }

        async function refresh(){
            await ensureTabId();
            // 校验：若当前 id 不在 GM_getTabs 返回集合中则重新生成
            try{
                if(typeof GM_getTabs === 'function'){
                    const tabs = await GM_getTabs();
                    let found = false;
                    for (const k in tabs){
                        const t = tabs[k];
                        if(t && t[TAB_KEY] === window.__SHIPIN_TAB_ID){
                            found = true; break;
                        }
                    }
                    if(!found){
                        const tab = await GM_getTab();
                        tab[TAB_KEY] = genId();
                        await GM_saveTab(tab);
                        emit(tab[TAB_KEY]);
                    }
                }
            }catch{}
        }

        window.__onShipinTabIdChange = fn => { if(typeof fn === 'function') LISTENERS.push(fn); };
        window.__getShipinTabId = () => window.__SHIPIN_TAB_ID;

        // 初次 & 定时
        refresh();
        setInterval(refresh, 4000);
        window.addEventListener('visibilitychange', () => { if(!document.hidden) refresh(); });
        window.addEventListener('focus', refresh);
    })();

})();